Golang sema
在 waitgroup 和 mutex 的源码中,都使用了一个 sema 信号量。
1 2 3 4
| type Mutex struct { state int32 sema uint32 }
|
1 2 3 4 5 6
| type WaitGroup struct { noCopy noCopy
state atomic.Uint64 sema uint32 }
|
今天就来看看这个 sema。
什么是信号量?
信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)发明的[1],广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。
当信号量为负数时,进程停止执行。其他情况则继续执行。
这里还需要提一下 PV 操作:
- P 操作,申请资源,信号量 –
- V 操作,释放资源,信号量 ++
而 sema 代表的是资源的状态,当 :
- sema > 0,资源充足,不需要排队
- sema = 0,资源紧张,需要竞争
- sema < 0,资源不足,需要等待
Golang sema 中有类似 PV 操作,它的核心是一个 uint32 类型的值,可以看作资源的数量。
一些源码解析
结构体
在 runtime/sema.go
中,可以找到 sema 的源码,以下是 sema 的底层结构体
1 2 3 4 5
| type semaRoot struct { lock mutex treap *sudog nwait atomic.Uint32 }
|
我们可以看到结构体有三个字段,其中需要介绍一下:
lock
,这是 runtime 包中的 lock,用来防止并发问题。
treap
,平衡树的根节点。从之前的文章中有介绍过,当我们有 goroutine 获取锁时,如果没有获取到,那么我们的 goroutine 会进入一个等待的状态。而 goroutine 呢就会被包装成一个 sudog 结构体放入到 treap 这个平衡树中进行等待。
nwait
,等待 goroutine 的数量,理论上说它的值与平衡树的节点值相等。
而 sudog 结构体部分定义如下:
1 2 3 4 5 6 7 8 9 10
| type sudog struct { g *g
next *sudog prev *sudog elem unsafe.Pointer …… waitlink *sudog …… }
|
其中:
elem
,是 sema 的地址
waitlink
,sudog 是一个平衡树的节点,而平衡树的节点实际上是一个队列。
sema 的控制
对于 sema 的控制,我们可以看代码(去掉了一些)↓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| func semacquire(addr *uint32) { semacquire1(addr, false, 0, 0, waitReasonSemacquire) }
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int, reason waitReason) { if cansemacquire(addr) { return }
s := acquireSudog() root := semtable.rootFor(addr) t0 := int64(0) s.releasetime = 0 s.acquiretime = 0 s.ticket = 0 if profile&semaBlockProfile != 0 && blockprofilerate > 0 { t0 = cputicks() s.releasetime = -1 } if profile&semaMutexProfile != 0 && mutexprofilerate > 0 { if t0 == 0 { t0 = cputicks() } s.acquiretime = t0 } for { lockWithRank(&root.lock, lockRankRoot) root.nwait.Add(1) if cansemacquire(addr) { root.nwait.Add(-1) unlock(&root.lock) break } root.queue(addr, s, lifo) goparkunlock(&root.lock, reason, traceEvGoBlockSync, 4+skipframes) if s.ticket != 0 || cansemacquire(addr) { break } } if s.releasetime > 0 { blockevent(s.releasetime-t0, 3+skipframes) } releaseSudog(s) }
|
可以看到这段代码可分为两部分:
- 资源充足,直接拿到资源并返回。
- 资源不充足,加入队列并循环等待,等待使要么是循环等待,要么是睡眠等待。
当等待结束后,会释放 sudog。也就是最后一行的 releaseSudog。
Reference
go中mutex的sema信号量是什么? - 掘金 (juejin.cn)
Go 底层锁:原子操作和sema信号量 - 掘金 (juejin.cn)